/**
 * MojoSetup; a portable, flexible installation application.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.
 *
       Copyright (c) 2006-2010 Ryan C. Gordon and others.

   This software is provided 'as-is', without any express or implied warranty.
   In no event will the authors be held liable for any damages arising from
   the use of this software.

   Permission is granted to anyone to use this software for any purpose,
   including commercial applications, and to alter it and redistribute it
   freely, subject to the following restrictions:

   1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software in a
   product, an acknowledgment in the product documentation would be
   appreciated but is not required.

   2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

   3. This notice may not be removed or altered from any source distribution.

       Ryan C. Gordon <icculus@icculus.org>
 *
 */

#include "fileio.h"
#include "platform.h"

#if !SUPPORT_UZ2
MojoArchive *MojoArchive_createUZ2(MojoInput *io) { return NULL; }
#else

// UZ2 format is a simple compressed file format used by UnrealEngine2.
//  it's just a stream of blocks like this:
//    uint32 compressed size
//    uint32 uncompressed size
//    uint8 data[compressed size]  <-- unpacks to (uncompressed size) bytes.
// Decompression is handled by zlib's "uncompress" function.

#include "miniz.h"

#define MAXCOMPSIZE 32768
#define MAXUNCOMPSIZE 33096  // MAXCOMPSIZE + 1%


// MojoInput implementation...

// Decompression is handled in the parent MojoInput, so this just needs to
//  make sure we stay within the bounds of the tarfile entry.

typedef struct UZ2input
{
    MojoInput *io;
    int64 fsize;
    uint64 position;
    uint32 compsize;
    uint8 compbuf[MAXCOMPSIZE];
    uint32 uncompsize;
    uint8 uncompbuf[MAXUNCOMPSIZE];
    uint32 uncompindex;
} UZ2input;

typedef struct UZ2info
{
    char *outname;
    int64 outsize;
    boolean enumerated;
} UZ2info;

static boolean unpack(UZ2input *inp)
{
    MojoInput *io = inp->io;
    uLongf ul = (uLongf) inp->uncompsize;

    // we checked these formally elsewhere.
    assert(inp->compsize > 0);
    assert(inp->uncompsize > 0);
    assert(inp->compsize <= MAXCOMPSIZE);
    assert(inp->uncompsize <= MAXUNCOMPSIZE);

    if (io->read(io, inp->compbuf, inp->compsize) != inp->compsize)
        return false;
    if (uncompress(inp->uncompbuf, &ul, inp->compbuf, inp->compsize) != Z_OK)
        return false;
    if (ul != ((uLongf) inp->uncompsize))  // corrupt data.
        return false;

    inp->uncompindex = 0;
    return true;
} // unpack

static boolean MojoInput_uz2_ready(MojoInput *io)
{
    UZ2input *input = (UZ2input *) io->opaque;
    if (input->uncompsize > 0)
        return true;
    return true;  // !!! FIXME: need to know we have a full compressed block.
} // MojoInput_uz2_ready

static int64 MojoInput_uz2_read(MojoInput *io, void *_buf, uint32 bufsize)
{
    uint8 *buf = (uint8 *) _buf;
    UZ2input *input = (UZ2input *) io->opaque;
    int64 retval = 0;
    while (bufsize > 0)
    {
        const uint32 available = input->uncompsize - input->uncompindex;
        const uint32 cpy = (available < bufsize) ? available : bufsize;
        if (available == 0)
        {
            if (input->position == input->fsize)
                return 0;
            else if (!MojoInput_readui32(input->io, &input->compsize))
                return (retval == 0) ? -1 : retval;
            else if (!MojoInput_readui32(input->io, &input->uncompsize))
                return (retval == 0) ? -1 : retval;
            else if (!unpack(input))
                return (retval == 0) ? -1 : retval;
            continue;  // try again.
        } // if

        memcpy(buf, input->uncompbuf + input->uncompindex, cpy);
        buf += cpy;
        bufsize -= cpy;
        retval += cpy;
        input->uncompindex += cpy;
        input->position += cpy;
    } // while

    return retval;
} // MojoInput_uz2_read

static boolean MojoInput_uz2_seek(MojoInput *io, uint64 pos)
{
    UZ2input *input = (UZ2input *) io->opaque;
    int64 seekpos = 0;

    // in a perfect world, this wouldn't seek from the start if moving
    //  forward. But oh well.
    input->position = 0;
    while (input->position < pos)
    {
        if (!input->io->seek(input->io, seekpos))
            return false;
        else if (!MojoInput_readui32(io, &input->compsize))
            return false;
        else if (!MojoInput_readui32(io, &input->uncompsize))
            return false;

        // we checked these formally elsewhere.
        assert(input->compsize > 0);
        assert(input->uncompsize > 0);
        assert(input->compsize <= MAXCOMPSIZE);
        assert(input->uncompsize <= MAXUNCOMPSIZE);

        input->position += input->uncompsize;
        seekpos += (sizeof (uint32) * 2) + input->compsize;
    } // while

    // we are positioned on the compressed block that contains the seek target.
    if (!unpack(input))
        return false;

    input->position -= input->uncompsize;
    input->uncompindex = (uint32) (pos - input->position);
    input->position += input->uncompindex;

    return true;
} // MojoInput_uz2_seek

static int64 MojoInput_uz2_tell(MojoInput *io)
{
    return (int64) (((UZ2input *) io->opaque)->position);
} // MojoInput_uz2_tell

static int64 MojoInput_uz2_length(MojoInput *io)
{
    return ((UZ2input *) io->opaque)->fsize;
} // MojoInput_uz2_length

static MojoInput *MojoInput_uz2_duplicate(MojoInput *io)
{
    MojoInput *retval = NULL;
    UZ2input *input = (UZ2input *) io->opaque;
    MojoInput *newio = input->io->duplicate(input->io);

    if (newio != NULL)
    {
        UZ2input *newopaque = (UZ2input *) xmalloc(sizeof (UZ2input));
        newopaque->io = newio;
        newopaque->fsize = input->fsize;
        // everything else is properly zero'd by xmalloc().
        retval = (MojoInput *) xmalloc(sizeof (MojoInput));
        memcpy(retval, io, sizeof (MojoInput));
        retval->opaque = newopaque;
    } // if

    return retval;
} // MojoInput_uz2_duplicate

static void MojoInput_uz2_close(MojoInput *io)
{
    UZ2input *input = (UZ2input *) io->opaque;
    input->io->close(input->io);
    free(input);
    free(io);
} // MojoInput_uz2_close


// MojoArchive implementation...

static boolean MojoArchive_uz2_enumerate(MojoArchive *ar)
{
    UZ2info *info = (UZ2info *) ar->opaque;
    MojoArchive_resetEntry(&ar->prevEnum);
    info->enumerated = false;
    return true;
} // MojoArchive_uz2_enumerate


static const MojoArchiveEntry *MojoArchive_uz2_enumNext(MojoArchive *ar)
{
    UZ2info *info = (UZ2info *) ar->opaque;

    MojoArchive_resetEntry(&ar->prevEnum);
    if (info->enumerated)
        return NULL;  // only one file in this "archive".

    ar->prevEnum.perms = MojoPlatform_defaultFilePerms();
    ar->prevEnum.filesize = info->outsize;
    ar->prevEnum.filename = xstrdup(info->outname);
    ar->prevEnum.type = MOJOARCHIVE_ENTRY_FILE;

    info->enumerated = true;
    return &ar->prevEnum;
} // MojoArchive_uz2_enumNext


static MojoInput *MojoArchive_uz2_openCurrentEntry(MojoArchive *ar)
{
    UZ2info *info = (UZ2info *) ar->opaque;
    MojoInput *io = NULL;
    UZ2input *opaque = NULL;
    MojoInput *dupio = NULL;

    if (!info->enumerated)
        return NULL;

    dupio = ar->io->duplicate(ar->io);
    if (dupio == NULL)
        return NULL;

    opaque = (UZ2input *) xmalloc(sizeof (UZ2input));
    opaque->io = dupio;
    opaque->fsize = info->outsize;
    // rest is zero'd by xmalloc().

    io = (MojoInput *) xmalloc(sizeof (MojoInput));
    io->ready = MojoInput_uz2_ready;
    io->read = MojoInput_uz2_read;
    io->seek = MojoInput_uz2_seek;
    io->tell = MojoInput_uz2_tell;
    io->length = MojoInput_uz2_length;
    io->duplicate = MojoInput_uz2_duplicate;
    io->close = MojoInput_uz2_close;
    io->opaque = opaque;

    return io;
} // MojoArchive_uz2_openCurrentEntry


static void MojoArchive_uz2_close(MojoArchive *ar)
{
    UZ2info *info = (UZ2info *) ar->opaque;
    MojoArchive_resetEntry(&ar->prevEnum);
    ar->io->close(ar->io);
    free(info->outname);
    free(info);
    free(ar);
} // MojoArchive_uz2_close


// Unfortunately, we have to walk the whole file, but we don't have to actually
//  do any decompression work here. Just seek, read 8 bytes, repeat until EOF.
static int64 calculate_uz2_outsize(MojoInput *io)
{
    int64 retval = 0;
    uint32 compsize = 0;
    uint32 uncompsize = 0;
    int64 pos = 0;

    if (!io->seek(io, 0))
        return -1;

    while (MojoInput_readui32(io, &compsize))
    {
        if (!MojoInput_readui32(io, &uncompsize))
            return -1;
        else if ((compsize > MAXCOMPSIZE) || (uncompsize > MAXUNCOMPSIZE))
            return -1;
        else if ((compsize == 0) || (uncompsize == 0))
            return -1;
        retval += uncompsize;
        pos += (sizeof (uint32) * 2) + compsize;
        if (!io->seek(io, pos))
            return -1;
    } // while

    if (!io->seek(io, 0))  // make sure we're back to the start.
        return -1;

    return retval;
} // calculate_uz2_outsize


MojoArchive *MojoArchive_createUZ2(MojoInput *io, const char *origfname)
{
    MojoArchive *ar = NULL;
    const char *fname = NULL;
    char *outname = NULL;
    size_t len = 0;
    int64 outsize = 0;
    UZ2info *uz2info = NULL;

    // There's no magic in a UZ2 that allows us to identify the format.
    //  The higher-level won't call this unless the file extension in
    //  (origfname) is ".uz2"

    // Figure out the output name ("x.uz2" would produce "x").
    if (origfname == NULL)
        return NULL;  // just in case.
    fname = strrchr(origfname, '/');
    if (fname == NULL)
        fname = origfname;
    else
        fname++;

    len = strlen(fname) - 4;  // -4 == ".uz2"
    if (strcasecmp(fname + len, ".uz2") != 0)
        return NULL;  // just in case.

    outsize = calculate_uz2_outsize(io);
    if (outsize < 0)
        return NULL;  // wasn't really a uz2? Corrupt/truncated file?
    outname = (char *) xmalloc(len+1);
    memcpy(outname, fname, len);
    outname[len] = '\0';

    uz2info = (UZ2info *) xmalloc(sizeof (UZ2info));
    uz2info->enumerated = false;
    uz2info->outname = outname;
    uz2info->outsize = outsize;

    ar = (MojoArchive *) xmalloc(sizeof (MojoArchive));
    ar->opaque = uz2info;
    ar->enumerate = MojoArchive_uz2_enumerate;
    ar->enumNext = MojoArchive_uz2_enumNext;
    ar->openCurrentEntry = MojoArchive_uz2_openCurrentEntry;
    ar->close = MojoArchive_uz2_close;
    ar->io = io;
    return ar;
} // MojoArchive_createUZ2

#endif // SUPPORT_UZ2

// end of archive_uz2.c ...

